package br.com.tcc.systemdx.service.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import br.com.tcc.systemdx.domain.AtributoGenerico;
import br.com.tcc.systemdx.domain.AtributoSintoma;
import br.com.tcc.systemdx.domain.DescricaoDoenca;
import br.com.tcc.systemdx.domain.FatorRelevante;
import br.com.tcc.systemdx.domain.Sintoma;
import br.com.tcc.systemdx.domain.ValorGenerico;
import br.com.tcc.systemdx.domain.ValorSintoma;
import br.com.tcc.systemdx.persistence.AtributoSintomaDAO;
import br.com.tcc.systemdx.persistence.DescricaoDoencaDAO;
import br.com.tcc.systemdx.persistence.SintomaDAO;
import br.com.tcc.systemdx.persistence.ValorSintomaDAO;
import br.com.tcc.systemdx.service.MecanismoInferenciaService;
import br.com.tcc.systemdx.service.beans.DoencasSemelhantesWrapper;
import br.com.tcc.systemdx.service.beans.GeradorConjuntos;
import br.com.tcc.systemdx.service.beans.QuestaoHipotese;

@Service
@Scope("singleton")
@Transactional
public class MecanismoInferenciaServiceImpl implements
		MecanismoInferenciaService, Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 4874836819012275334L;
	
	private Set<DescricaoDoenca> escopoDiagnostico;
	
	private Set<Sintoma> manifestacoes;
	
	private Set<FatorRelevante> fatoresPresentes;
	
	private Set<GeradorConjuntos> foco;
	
	private List<QuestaoHipotese> questoesRealizadas;
	
	private boolean cicloHipoteseFinalizado;
	
	@Autowired
	private SintomaDAO sintomaDAO;
	@Autowired
	private DescricaoDoencaDAO descricaoDoencaDAO;
	@Autowired
	private ValorSintomaDAO valorSintomaDAO;
	@Autowired
	private AtributoSintomaDAO atributoSintomaDAO;

	
	public void inicializarCicloTesteHipoteses() {
		inicializarDependencias();
	}
	
	/**
	 * Inicializa as principais estruturas de dados para 
	 * o funcionamento do mecanismo de inferencia
	 */
	private void inicializarDependencias() {
		escopoDiagnostico = new HashSet<DescricaoDoenca>();
		manifestacoes = new HashSet<Sintoma>();
		foco = new HashSet<GeradorConjuntos>();
		fatoresPresentes = new HashSet<FatorRelevante>();
		cicloHipoteseFinalizado = false;
		questoesRealizadas = new ArrayList<QuestaoHipotese>();
	}


	public void responderQuestaoHipoteseSintoma(QuestaoHipotese questaoRespondida) {
		executarCicloTesteHipotese((AtributoSintoma)questaoRespondida.getAtributo(), (ValorSintoma)questaoRespondida.getResposta());
	}
	
	public QuestaoHipotese obterNovaQuestaoHipoteseSintoma() {
		// Verifica se é a primeira questão
		QuestaoHipotese novaQuestao = null;
		
		if (questoesRealizadas == null || questoesRealizadas.isEmpty()) {
			questoesRealizadas = new ArrayList<QuestaoHipotese>();
		}
		// Busca o sintoma com maior número de ocorrência no foco
		
		// Agrupa as doenças do foco em um conjunto
		Set<DescricaoDoenca> doencas = new HashSet<DescricaoDoenca>();
		
		for (GeradorConjuntos gerador : foco) {
			doencas.addAll(gerador.getDoencas());
		}
		
		//Conta a ocorrência do primeiro sintoma não verificado de cada doença
		Map<AtributoSintoma, Integer> contadorSintomas = new HashMap<AtributoSintoma, Integer>();
		for (DescricaoDoenca doenca : doencas) {
			for (Sintoma sintoma : doenca.getSintomas()) {
				if (!atributoQuestionado(sintoma.getAtributo())) {
					if (contadorSintomas.containsKey(sintoma.getAtributo())) {
						contadorSintomas.put(sintoma.getAtributo(), contadorSintomas.get(sintoma.getAtributo()) + 1);
					} else {
						contadorSintomas.put(sintoma.getAtributo(), 1);
					}
					break;
				}
			}
		}
		
		// Pega o sintoma com maior ocorrência para a próxima pergunta
		int maiorOcorrencia = 0;
		AtributoSintoma atributo = null;
		for(Map.Entry<AtributoSintoma, Integer> entry : contadorSintomas.entrySet()) {
			if (entry.getValue() > maiorOcorrencia) {
				maiorOcorrencia = entry.getValue();
				atributo = entry.getKey();
			}
		}
		
		if (maiorOcorrencia > 0) {
			// Monta a nova questão com o atributo
			novaQuestao = new QuestaoHipotese();
			novaQuestao.setAtributo(atributo);
			
			List<ValorSintoma> valoresSintoma = valorSintomaDAO.findValoresPossiveisAtributo(atributo.getId());
			List<ValorGenerico> alternativas = new ArrayList<ValorGenerico>();
			for (ValorSintoma valor : valoresSintoma) {
				alternativas.add(valor);
			}
			novaQuestao.setAlternativas(alternativas);
			
			// Adiciona na lista de questões realizadas
			questoesRealizadas.add(novaQuestao);
			
			return novaQuestao;
		} else {
			// Critério de parada: todos os sintomas possíveis foram verificados
			cicloHipoteseFinalizado = true;
			return null;
		}
	}
	
	private boolean atributoQuestionado(AtributoGenerico atributo) {
		for (QuestaoHipotese questao : questoesRealizadas) {
			if (questao.getAtributo().equals(atributo))
				return true;
		}
		return false;
	}
	
	public boolean isCicloTesteHipoteseEmAndamento() {
		if (escopoDiagnostico != null &&
				manifestacoes != null &&
				foco != null &&
				fatoresPresentes != null &&
				cicloHipoteseFinalizado == false)
			return true;
		
		return false;
	}


	
	public boolean isCicloTesteHipoteseFinalizado() {
		return cicloHipoteseFinalizado;
	}

	public void inicializarCicloTesteHipoteses(Long atributoSintomaId, Long valorSintomaId) {
		AtributoSintoma atributo = atributoSintomaDAO.findById(atributoSintomaId);
		ValorSintoma valor = valorSintomaDAO.findById(valorSintomaId);
		// Inicializa-se todas as dependências do mecanismo de infer�ncia
		inicializarDependencias();
		// Registra o atributo selecionado como atributo questionado para evitar perguntas repetidas
		QuestaoHipotese questaoHipotese = new QuestaoHipotese();
		questaoHipotese.setAtributo(atributo);
		questaoHipotese.setResposta(valor);
		if (questoesRealizadas == null) {
			questoesRealizadas = new ArrayList<QuestaoHipotese>();
		}
		questoesRealizadas.add(questaoHipotese);
		// Executa-se o ciclo teste de hipótese
		executarCicloTesteHipotese(atributo, valor);
		
	}
	
	public void executarCicloTesteHipotese(AtributoSintoma atributo, ValorSintoma valor) {
		// 1) Obteve-se sintoma
		// 2) Encontrando causas da base de conhecimento: causas(sintoma)
		List<DescricaoDoenca> doencas = descricaoDoencaDAO.findByAtributoIdValorId(atributo.getId(), valor.getId());
		
		// 3) manifestações <- manifestações U {sintoma}
		// Cria-se um novo sintoma para guardar o atributo e valor recebido
		Sintoma sintoma = new Sintoma();
		sintoma.setAtributo(atributo);
		sintoma.setValor(valor);
		sintoma.setElaboracao(Collections.EMPTY_LIST);
		manifestacoes.add(sintoma);
		
		// 4) Escopo <- Escopo U causas(sintoma)
		escopoDiagnostico.addAll(doencas);
		
		// 5) Ajustar foco para acomodar sintoma
		ajustarFoco(doencas);
		
		// 6) Processo continuado pela chamada da próxima questão
	}
	
	/**
	 * Ajusta o foco durante o processamento do mecanismo de inferência.
	 * Assume que todas as dependências já foram inicializadas.
	 */
	private void ajustarFoco(List<DescricaoDoenca> doencasNovoSintoma) {
		if (foco.isEmpty()) {
			// Se o foco estiver vazio, a intersecção deste com escopoDiagnóstico
			// também seria vazia, então, nesse caso, adiciona-se todos os itens do 
			// escopoDiagnostico no foco.
			GeradorConjuntos novoGerador = new GeradorConjuntos();
			
			// Cria-se um novo item para um novo gerador
			List<Set<DescricaoDoenca>> itemGerador = new ArrayList<Set<DescricaoDoenca>>();
			itemGerador.add(new HashSet<DescricaoDoenca>(escopoDiagnostico));
			// Conclui-se a criação do novo gerador
			novoGerador.setElementosProduto(itemGerador);
			
			// Adiciona-se o novo gerador no foco
			foco.add(novoGerador);
		} else {
			// Verifica-se se há a possibilidade de efetuar o ajuste pelo 
			// método da intersecção: foco com um único gerador sendo este um gerador simples
			// (também com um único elemento).
			if (foco.size() == 1) {
				Iterator iter = foco.iterator();
				GeradorConjuntos gerador = (GeradorConjuntos)iter.next();
				if (gerador.isGeradorSimples()) {
					Set<DescricaoDoenca> doencasFoco = gerador.getElementosProduto().get(0);
					// Realiza a intersecção
					doencasFoco.retainAll(doencasNovoSintoma);
					if (doencasFoco.size() == 0) {
						// Faz o reajuste pelo método dos geradores
						ajustarFocoPorGeradores();
					}
				} else {
					ajustarFocoPorGeradores();
				}
			} else {
				// Faz o reajuste pelo método dos geradores
				ajustarFocoPorGeradores();
			}
		}
	}
	
	private void ajustarFocoPorGeradores() {
		// Cria-se mapa de doenças por sintomas 
		Map<DescricaoDoenca, List<Sintoma>> doencasSintomas = new HashMap<DescricaoDoenca, List<Sintoma>>();
		int maiorCardinalidade = 0;
		
		for (DescricaoDoenca doenca : escopoDiagnostico) {
			List<Sintoma> sintomasDoenca = new ArrayList<Sintoma>();
			
			for (Sintoma sintoma : doenca.getSintomas()) {
				for (Sintoma sintomaManifestacoes :  manifestacoes) {
					if (sintoma.atributosIguais(sintomaManifestacoes)) {
						sintomasDoenca.add(sintomaManifestacoes);
						break;
					}
				}
			}
			
			doencasSintomas.put(doenca, sintomasDoenca);
			
			if (sintomasDoenca.size() > maiorCardinalidade)
				maiorCardinalidade = sintomasDoenca.size(); 
		}
		
		List<DoencasSemelhantesWrapper> listaDoencasSemelhantes = null;
		
		// Agrupa doencas do mapa em doencas semelhantes para facilitar o processamento
		for (Map.Entry<DescricaoDoenca, List<Sintoma>> entry : doencasSintomas.entrySet()) {
			if (listaDoencasSemelhantes == null) {
				listaDoencasSemelhantes = new ArrayList<DoencasSemelhantesWrapper>();
				DoencasSemelhantesWrapper doencasSemelhantes = new DoencasSemelhantesWrapper(entry.getKey(), entry.getValue(), manifestacoes);
				listaDoencasSemelhantes.add(doencasSemelhantes);
			} else {
				boolean doencaNaoAcrescentada = true;
	
				for (DoencasSemelhantesWrapper doencas : listaDoencasSemelhantes) {
					if (doencas.isDoencaSemelhante(entry.getKey(), entry.getValue()))
						doencaNaoAcrescentada = false;
				}
				
				if (doencaNaoAcrescentada) {
					DoencasSemelhantesWrapper doencasSemelhantes = new DoencasSemelhantesWrapper(entry.getKey(), entry.getValue(), manifestacoes);
					listaDoencasSemelhantes.add(doencasSemelhantes);
				}
			}
		}
		
		int cardinalidadeAtual = maiorCardinalidade;
		Set<GeradorConjuntos> novoFoco = null;
		
		if (cardinalidadeAtual > 1) {
			int controleCF = 0;
			while (cardinalidadeAtual > 1 && controleCF < 100) {
				controleCF++;
				// Procuro pelo primeiro item das doenças semelhante com cardinalidade de sintomas maior
				// e retiro da lista para comparação com os outros
				DoencasSemelhantesWrapper doencasIniciais = null;
				int indiceDoencasRetiradas = 0;
				for (int i = 0; i < listaDoencasSemelhantes.size(); i++) {
					if (listaDoencasSemelhantes.get(i).getNumeroSintomasSemelhantes() == cardinalidadeAtual) {
						doencasIniciais = listaDoencasSemelhantes.get(i);
						indiceDoencasRetiradas = i;
						break;
					}
				}
				
				if (doencasIniciais == null) {
					// Não encontrou nenhuma doenca para a cardinalidade referida
					cardinalidadeAtual--;
					continue;
				}
				
				// retira da lista para evitar comparações repetidas
				listaDoencasSemelhantes.remove(indiceDoencasRetiradas);
				
				// Tento criar um novo gerador com as doencas obtidas
				GeradorConjuntos novoGerador = new GeradorConjuntos();
				int noSintomasFaltantes = doencasIniciais.getNumeroSintomasFaltantes();
				boolean geradorCompleto = false;
				List<DoencasSemelhantesWrapper> doencasProduto = new ArrayList<DoencasSemelhantesWrapper>();
				int controleCG = 0; 
				
				while (!geradorCompleto && noSintomasFaltantes > 0 && controleCG < 100) {
					int tamListaInicio = doencasProduto.size();
					controleCG++; // Evita-se loops infinitos
					// Agrupo em uma lista as doenças que possuem os sintomas complementares das doenças iniciais
					// e não possuem doenças repetidas
					for (DoencasSemelhantesWrapper doencas : listaDoencasSemelhantes) {
						int noComplementares = doencasIniciais.numeroSintomasComplementares(doencas.getSintomasSemelhantes());
						if (noComplementares == noSintomasFaltantes && !doencasProduto.contains(doencas)) {
							doencasProduto.add(doencas);
						}
					}
					
					Set<Sintoma> sintomasAbrangidos = new HashSet<Sintoma>(doencasIniciais.getSintomasSemelhantes());
					for (DoencasSemelhantesWrapper doencas : doencasProduto) {
						sintomasAbrangidos.addAll(doencas.getSintomasSemelhantes());
					}
					
					noSintomasFaltantes = manifestacoes.size() - sintomasAbrangidos.size();
					if (noSintomasFaltantes == 0)
						geradorCompleto = true;
					else if (doencasProduto.size() == tamListaInicio)
						noSintomasFaltantes--;
					
					if (novoGerador.getElementosProduto() == null) {
						List<Set<DescricaoDoenca>> elementosProduto = new ArrayList<Set<DescricaoDoenca>>();
						elementosProduto.add(new HashSet<DescricaoDoenca>(doencasIniciais.getDoencasSemelhantes()));
						novoGerador.setElementosProduto(elementosProduto);
					}
					
					// Mapa que agrupa as doenças pelos sintomas complementares aos das doenças iniciais 
					// para posterior inserção no gerador de conjuntos
					Map<List<Sintoma>, List<DescricaoDoenca>> mapaDoencasSintomas = new HashMap<List<Sintoma>, List<DescricaoDoenca>>(); 
					
					for (DoencasSemelhantesWrapper doencas : doencasProduto) {
						// Monta a lista
						List<Sintoma> sintomasComplementares = new ArrayList<Sintoma>();
						for (Sintoma sintoma : doencas.getSintomasSemelhantes()) {
							for (Sintoma sintomaInicial : doencasIniciais.getSintomasFaltantes()) {
								if (sintoma.atributosIguais(sintomaInicial)) 
									sintomasComplementares.add(sintoma);
							}
						}
						
						if (mapaDoencasSintomas.containsKey(sintomasComplementares)) {
							mapaDoencasSintomas.get(sintomasComplementares).addAll(doencas.getDoencasSemelhantes());
						} else {
							mapaDoencasSintomas.put(sintomasComplementares, new ArrayList<DescricaoDoenca>(doencas.getDoencasSemelhantes()));
						}
					}
					
					for (Map.Entry<List<Sintoma>, List<DescricaoDoenca>> entry : mapaDoencasSintomas.entrySet()) {
						// Não insere elementos que já estão na lista
						Set<DescricaoDoenca> setDoencas = new HashSet<DescricaoDoenca>(entry.getValue());
						if (!novoGerador.getElementosProduto().contains(setDoencas))
							novoGerador.getElementosProduto().add(setDoencas);
					}
					
					novoGerador.setSintomasAbrangidos(sintomasAbrangidos);
				}
				
				if (geradorCompleto) {
					if (novoFoco == null)
						novoFoco = new HashSet<GeradorConjuntos>();
					novoFoco.add(novoGerador);
				}
			}
		} else if (cardinalidadeAtual == 1) {
			GeradorConjuntos novoGerador = new GeradorConjuntos();
			novoGerador.setElementosProduto(new ArrayList<Set<DescricaoDoenca>>());
			for (DoencasSemelhantesWrapper doencas : listaDoencasSemelhantes) {
				novoGerador.getElementosProduto().add(new HashSet<DescricaoDoenca>(doencas.getDoencasSemelhantes()));
			}
			if (novoFoco == null)
				novoFoco = new HashSet<GeradorConjuntos>();
			novoFoco.add(novoGerador);
		}
		
		if (novoFoco != null)
			foco = novoFoco;
	}

	public Set<DescricaoDoenca> getEscopoDiagnostico() {
		return escopoDiagnostico;
	}

	public Set<Sintoma> getManifestacoes() {
		return manifestacoes;
	}

	public Set<FatorRelevante> getFatoresPresentes() {
		return fatoresPresentes;
	}

	public Set<GeradorConjuntos> getFoco() {
		return foco;
	}

}
